透過先前的文章,我們首先認識了 Grafana Scenes 如何建構起一個 Plugin,接著透過前端可觀測性了解 Grafana Faro 在前端應用程式中進行數據資料收集的過程,也了解 Grafana Cloud 中的前端可觀測性平台 - Frontend Observability 提供的所有介面及功能。既然已經了解兩個重要的角色,接下來我們就要試著以 Grafana Scenes 建構起一個前端可觀測性平台。
Grafana Cloud 的可觀測性平台提供給我們一個建構自己團隊專屬的啟發,但我們的需求不一定跟 Grafana Cloud 的相同,我們只能被動的使用他們提供的介面,所以回到本系列第一篇文章的宗旨,希望可以透過 Grafana Scenes 打造各種客製化的應用程式,甚至不限於可觀測性或監控平台,把他當作一個擁有 Grafana 介面的 React 框架來使用。
在本系列文的最後兩篇文章中,會向各位讀者展示如何透過 Grafana Scenes 打造一個前端可觀測性平台,我們不會逐一的手把手地印出所有程式碼,而是會著重在介面與功能的實現,以及資料的取得方法和元件的使用,讓我們開始分析這個平台的實現方法吧!
在第二十七天的文章中,先帶大家了解這個前端可觀測性平台的全貌,這個平台主要分成三個部分:
但我們不貪心,先從綜觀的 Overview 頁面開始實作。在這個客製化的應用程式中,我們全部都使用 Grafana Scenes 所提供的元件,從數據取得、資料轉換、變數設置以及視覺話的呈現,當然很多 Panel 的類型不是由 Visualization 中的元件所提供,所以需要使用 Grafana UI 的元件來實現。
除了應用程式的整體 Layout 之外,我會將這個頁面分為四個部分做實作觀念的介紹。
💡 NOTICE
在重現這個平台的過程中,最重要的是確保 UI 呈現和資料取得的正確性。資料可以透過 Grafana Cloud 中的各種查詢請求來獲取,因此只需要打開瀏覽器的 network 介面,就能複製各種查詢請求,並應用到客製化的應用程式中。
上圖區塊是整個應用程式的 Layout,所有的 UI 元件都可以使用 Scenes 提供的元件完成,比較需要注意的是 Overview、Errors、Sessions 三個頁面需要由 tabs 屬性來設置,另外 $timeRange
和 $variables
是應用程式進行資料查詢時,必須帶入的變數,也需要在 Layout 中設置,大概的結構會如下圖呈現。
變數設置是 Layout 中很重要的邏輯,會與 control 元件一起組成 Logs 的下拉選單,這個元件主要呈現 Loki DataSource 的來源,如果需求有很多座 Loki 服務,可以透過這個元件選擇資料查詢的來源,因此此處會使用 DataSourceVariable
設置變數,而初始化的設定如果不清楚 uid 的值,可以透過 getDataSourceSrv
方法取得,並使用 addActivationHandler
設置變數的初始值。
const datasourceVariable = new DataSourceVariable({
name: "loki",
label: "Logs",
value: "",
pluginId: "loki",
});
datasourceVariable.addActivationHandler(() => {
const bes = getDataSourceSrv().getList({ filter: (ds) => ds.type === "loki" });
const options = bes.map((be) => ({ label: be.name, value: be.uid }));
datasourceVariable.setState({ value: options[0].value, text: options[0].label });
});
第一部分是 Page Loads、Errors 以及所有 Web Vitals 的測量數據,請求的 query type 會以 instant 作為設定,而 UI 的部分 Page Loads、Errors 可以以 stat 的 Panel 呈現,但 Web Vitals 的樣式會需要自己實作,需要自定義 SceneObject 的元件,其中包含數值的呈現,還有非常一目瞭然的 Progress Bar,並且需要依據不同的閾值設定不同的顏色。
📝TIP
覺得這部分最有挑戰的是 Progress Bar 的百分比定義,因為閾值沒有上限,所以需要在每段區間中設置不同的百分比,並以 33% 和 66% 作為分界點,也要注意超過 100% 時要以 100% 呈現,讓指針可以停留在 progress bar 的末端。
另外 Page Loads、Errors 雖然可以使用 stat 的 Panel 呈現,但如果追求閾值顏色的顯示,可以使用 setThresholds 的方法來設定:
setThresholds({
mode: ThresholdsMode.Absolute,
steps: [
{ value: 0, color: measurement.key === "errors" ? "green" : "blue" },
{ value: 1, color: measurement.key === "errors" ? "red" : "blue" },
],
});
第二部分是看似簡單的 Page Loads 和 Errors 的 Panel,不是單純地取得資料後將數值顯示在 time series 的 Panel 上,而是需要將線圖改為 bar chart,並以不同的顏色呈現,還有將 legend 的顯示名稱修改,這些都是透過 setCustomFieldConfig
和 setOverrides
的方法來完成。而注意右上方的 Explore 按鈕,這邊會使用 LinkButton
的元件,並以 setHeaderActions 的方法設置:
.setCustomFieldConfig('drawStyle', GraphDrawStyle.Bars) // 將線圖改為 bar chart
.setCustomFieldConfig('stacking', { mode: StackingMode.Normal }) // 將 bar 呈現堆疊
.setCustomFieldConfig('barWidthFactor', 1) // 設置 bar 的寬度
.setOverrides((b) => {
b.matchFieldsWithNameByRegex('/exception/')
.overrideDisplayName('Errors') // 修改 legend 的顯示名稱
.overrideColor({ mode: FieldColorModeId.Fixed, fixedColor: 'red' }) // 設置為紅色
.matchFieldsWithNameByRegex('/measurement/')
.overrideDisplayName('Page Loads')
.overrideColor({ mode: FieldColorModeId.Fixed, fixedColor: 'blue' });
})
.setHeaderActions(
<LinkButton
size="sm"
icon="compass"
variant="secondary"
href={`/explore?left=${encodeURIComponent(JSON.stringify(queries))}`}
>
Explore
</LinkButton>
);
第三部分是 Page Performance 的 Panel,數據的取得不難,會同時以 7 個 queries 進行請求,最具挑戰的是資料的轉換,以及 Table 的設置。Table 的功能及樣式無法以 PanelBuilder 提供的 Table 元件設置,必須使用自定義的 ScenesObject,並引入 Grafana UI 的 InteractiveTable 來實現,需要定義 columns (每個 table header 的欄位設置) 及 data (每一個 table row 的資料) 的屬性,以及使用 getRowId
的方法設置 row 的 id,才能在 Table 中顯示資料。
而資料的處理,會建議先將 dataFrame 的資料格式印出後,再依據 columns 的屬性進行資料的轉換,其中包括數值的格式化、預設值的設定。而在 SceneObject 中因為需要時間差取得原始的資料,所以資料的 format 會在 addActivationHandler 中偵測到狀態有改變時再進行初始化的設定,例如下面的範例:
this.addActivationHandler(() => this._onActivate());
public _onActivate() {
const dataProvider = sceneGraph.getData(this);
this._subs.add(
dataProvider.subscribeToState((state) => {
const rawData = state?.data?.series;
const data = rawData ? getTableData(rawData) : [];
this.setState({ tableData: data });
})
);
return () => {
this._subs.unsubscribe();
};
}
另外,Grafana Cloud 中有一些數值呈現的小巧思,例如數值會根據不同的閾值設定對應的顏色。相同的邏輯可以參考第一部分 Web Vitals 的 Progress Bar 實作。而在 Page ID 欄位中,數值會以連結形式呈現,目的是為了顯示該頁面的詳細資料。這些客製化的 table cell 都可以在 columns 中設定,並根據傳遞的 value 進行客製化處理,例如:
{
id: 'pageId',
header: 'Page ID',
sortType: 'alphanumeric',
cell: ({ value }) => (
<Button
variant="primary"
fill="text"
onClick={() => toggleAction(value)}
>
{value}
</Button>
),
}
💡NOTICE
底層是以react-table
實現,但 Grafana 封裝後的元件提供的參數較少,目前實測還無法自定義每個 row 的樣式設定,但透過 cell 的客製化可以完成許多複雜的變化。還是希望未來可以有更多自定義的空間。
在實作前面三個部分後,會發現第四部分的實作相對簡單。三個 time series 的 Panel 可以互相覆用,資料取得也不需要做額外轉換。這些資料分別透過三組 queries 來獲取:[ttfb、fcp、lcp]、[cls]、[fid、inp],並使用 range 類型的查詢來獲取數據,將這些 web vitals 根據其特性分組顯示。
需要特別注意的是折線圖的呈現方式。預設情況下,折線圖會以資料點來顯示,我們需要使用 setCustomFieldConfig 方法將 drawStyle 設定為 Line,並另外設定 lineWidth 和 spanNulls 屬性,才能讓折線圖顯示得更加平滑且連貫。
.setCustomFieldConfig('drawStyle', GraphDrawStyle.Line)
.setCustomFieldConfig('lineWidth', 2)
.setCustomFieldConfig('spanNulls', true)
最後,有一個特別的設置——$behavior
,在第十六天的文章中有提到。這裡會使用 CursorSync
的功能,使三個 Panel 以及 PageLoads and Errors 的時間軸同步,並以 Tooltip
的方式呈現,讓使用者能清楚查看四個 Panel 同一個資料點的 tooltip 資訊。而要實現跨 Panel 的時間軸同步,必須透過 key 設置唯一的識別字串,才能在 CursorSync 中進行設置。
$behaviors: [new behaviors.CursorSync({ key: 'time-series-panel', sync: DashboardCursorSync.Tooltip })],
筆者語錄
靠著 Grafana Scenes 的元件,可以很方便的實現許多複雜的 UI 效果,在實現的過程中,其實多少還是會遇到 Panel 或是數據轉換的問題,所以多次查看前面幾十天的文章,回顧各種 SceneObject 的實作方法,才能順利解決問題。實現的宗旨也是希望可以復刻出 Grafana Cloud 的介面,甚至可以在這過程中發現官方的 bugs,當然我們可以透過這些客製化將功能優化,讓使用者有更好的體驗。而下篇的實作內容會著重在 Page Performance 的 Page ID 連結功能,實踐的方法需要連動網址、變數、請求查詢以及原始頁面,因此會有更多挑戰。